﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Common.Logging;

namespace DbExtendedProvider
{
	public abstract class Encryption<TRandomGenerator, THashAlgorithm, TEncryptingAlgorithm> : IEncryption 
		where TRandomGenerator : RandomNumberGenerator, new()
		where THashAlgorithm : HashAlgorithm, new()
		where TEncryptingAlgorithm : SymmetricAlgorithm, new()
	{
		protected const string SaltChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-";
		private static readonly ILog Log = LogManager.GetCurrentClassLogger();
		protected readonly int SaltCharLength = SaltChars.Length;
		protected readonly RandomNumberGenerator RandomGenerator;
		protected readonly HashAlgorithm HashGenerator;
		protected readonly SymmetricAlgorithm CryptoGenerator;

		protected Encryption()
			: this(null, null, null)
		{

		}

		protected Encryption(
			TRandomGenerator randomNumberGenerator,
			THashAlgorithm hashAlgorithm,
			TEncryptingAlgorithm symmetricAlgorithm)
		{
			RandomGenerator = randomNumberGenerator ?? new TRandomGenerator();
			HashGenerator = hashAlgorithm ?? new THashAlgorithm();

			if (symmetricAlgorithm == null)
			{
				symmetricAlgorithm = new TEncryptingAlgorithm
					{
						Mode = CipherMode.CBC,
						Padding = PaddingMode.PKCS7
					};
			}
			CryptoGenerator = symmetricAlgorithm;
		}

		public virtual string GenerateSalt(int minChars,
										   int maxChars)
		{
			Log.Debug(d => d("GenerateSalt({0}, {1})", minChars, maxChars));
			if (minChars <= 0) throw new ArgumentOutOfRangeException("minChars");
			if (maxChars < minChars) throw new ArgumentOutOfRangeException("maxChars");

			var saltLength = GenerateTrueRandomNumber(minChars, maxChars);
			var salt = string.Empty;
			for (var i = 0; i < saltLength; i++)
			{
				salt += SaltChars.Substring(GenerateTrueRandomNumber(0, SaltCharLength), 1);
			}
			return salt;
		}

		public virtual int GenerateTrueRandomNumber(int min,
													int max)
		{
			Log.Debug(d => d("GenerateTrueRandomNumber({0}, {1})", min, max));
			if (min == max) return min;
			var range = max - min;
			var buffer = BitConverter.GetBytes(range);
			RandomGenerator.GetBytes(buffer);
			var rndValue = (decimal)Math.Abs(BitConverter.ToInt32(buffer, 0));
			var integral = (decimal)int.MaxValue / range;
			var value = ((int)(rndValue / integral) + min);
			Log.Debug(d => d("Returning {0}", value));
			return value;
		}

		public virtual byte[] GenerateHash(string toHash,
										   string salt)
		{
			if (string.IsNullOrEmpty(toHash)) throw new ArgumentException("toHash");
			if (string.IsNullOrEmpty(salt)) throw new ArgumentException("salt");

			var bytes = Encoding.GetBytes(toHash + salt);
			return HashGenerator.ComputeHash(bytes);
		}

		public virtual byte[] EncryptString(byte[] toEncrypt,
											byte[] encryptionKey)
		{
			if (encryptionKey == null || encryptionKey.Length == 0) throw new ArgumentException("encryptionKey");
			CryptoGenerator.Key = encryptionKey;
			CryptoGenerator.GenerateIV();

			using (var encryptor = CryptoGenerator.CreateEncryptor(CryptoGenerator.Key, CryptoGenerator.IV))
			{
				using (var ms = new MemoryStream())
				{
					using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
					{
						cs.Write(toEncrypt, 0, toEncrypt.Length);
						cs.FlushFinalBlock();
						var retVal = new byte[16 + ms.Length];
						CryptoGenerator.IV.CopyTo(retVal, 0);
						ms.ToArray().CopyTo(retVal, 16);
						return retVal;
					}
				}
			}
		}

		public virtual byte[] EncryptString(string toEncrypt,
											byte[] encryptionKey)
		{
			if (string.IsNullOrEmpty(toEncrypt)) throw new ArgumentException("toEncrypt");
			return EncryptString(Encoding.GetBytes(toEncrypt), encryptionKey);
		}

		public virtual byte[] DecryptString(byte[] encryptedString,
											byte[] encryptionKey)
		{
			if (encryptedString == null || encryptedString.Length == 0) throw new ArgumentException("encryptedString");
			if (encryptionKey == null || encryptionKey.Length == 0) throw new ArgumentException("encryptionKey");

			CryptoGenerator.Key = encryptionKey;
			CryptoGenerator.IV = encryptedString.Take(16).ToArray();
			using (var ms = new MemoryStream(encryptedString, 16, encryptedString.Length - 16))
			{
				using (var decryptor = CryptoGenerator.CreateDecryptor(CryptoGenerator.Key, CryptoGenerator.IV))
				{
					using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
					{
						var decrypted = new byte[encryptedString.Length];
						var byteCount = cs.Read(decrypted, 0, encryptedString.Length);
						Log.Trace(t => t("Decrypted Byte Count: {0}", byteCount));
						return decrypted;
					}
				}
			}
		}

		public virtual byte[] GenerateKey()
		{
			CryptoGenerator.GenerateKey();
			return CryptoGenerator.Key;
		}

		public virtual void Dispose()
		{
			if (RandomGenerator != null) RandomGenerator.Dispose();
			if (HashGenerator != null) HashGenerator.Dispose();
			if (CryptoGenerator != null) CryptoGenerator.Dispose();
		}

		public virtual bool ValidateKey(byte[] encryptionKey, params byte[][] supplementalKeys)
		{
			if (supplementalKeys != null)
				throw new NotSupportedException("This implementation does not support supplemental keys.");

			return encryptionKey != null
					&& CryptoGenerator.ValidKeySize(encryptionKey.Length);
		}

		public abstract Encoding Encoding { get; }
	}
}